﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

using BeachRunner.Utility;

namespace BeachRunner.Tileengine
{
    /// <summary>
    /// Datenstruktur die Kollisionsdaten aufnimmt
    /// 
    /// Autor: OLD (Mattis Hänel)
    /// </summary>
    public struct TraceResult
    {
        /// <summary>
        /// Die aus der Kollision resultierende Position
        /// </summary>
        public Vector2 resPosition;

        /// <summary>
        /// Der Index des Tiles mit dem in der Horizontalen kollidiert wurde.
        /// </summary>
        public int tileX;

        /// <summary>
        /// Der Index des Tiles mit dem in der Vertikalen kollidiert wurde.
        /// </summary>
        public int tileY;

        /// <summary>
        /// Gibt an ob es in der Horizontalen zu einer Kollision kam.
        /// </summary>
        public bool collisionX;

        /// <summary>
        /// Gibt an ob es in der Vertikalen zu einer Kollision kam.
        /// </summary>
        public bool collisionY;
    }

    /// <summary>
    /// Klasse zum halten von Karten
    /// 
    /// Autor: Mattis Hänel
    /// </summary>
    public class Map
    {
        /// <summary>
        /// Das von der Karte verwendete Tileset
        /// </summary>
        private Tileset tileset;

        /// <summary>
        /// Macht das von der Karte verwendete Tileset zugänglich.
        /// </summary>
        public Tileset Tileset
        {
            get { return tileset; }
        }

        /// <summary>
        /// Die Daten der einzelnen Ebenen
        /// </summary>
        private Layer[] layer;
        /// <summary>
        /// Macht die Daten der einzelnen Ebenen zugänglich.
        /// </summary>
        public Layer[] Layer
        {
            get { return layer; }
        }

        /// <summary>
        /// Die Ebene die die Kollisionsdaten enthält
        /// </summary>
        private Layer collisionLayer;
        /// <summary>
        /// Die Ebene die die Kollisionsdaten enthält
        /// </summary>
        public Layer CollisionLayer
        {
            get { return collisionLayer; }
            set { collisionLayer = value; }
        }

        /// <summary>
        /// Die Breite der Karte
        /// </summary>
        private int width;
        /// <summary>
        /// Macht die Breite der Karte zugänglich.
        /// </summary>
        public int Width
        {
            get { return width; }
        }

        /// <summary>
        /// Die Höhe der Karte
        /// </summary>
        private int height;

        /// <summary>
        /// Macht die Höhe der Karte zugänglich.
        /// </summary>
        public int Height
        {
            get { return height; }
        }

        /// <summary>
        /// Bildet Ebenename auf Ebeneindex ab
        /// </summary>
        private Dictionary<String, int> nameToLayer;
        /// <summary>
        /// Bildet Ebenename auf Ebeneindex ab
        /// </summary>
        public Dictionary<String, int> NameToLayer
        {
            get { return nameToLayer; }
        }

        /// <summary>
        /// Die Anzahl von Tiles einer Ebene
        /// </summary>
        private int length; 

        /// <summary>
        /// Erstellt eine lehre Karte
        /// </summary>
        /// <param name="width">Die Breite der Karte in Tiles</param>
        /// <param name="height">Die Höhe der Karte in Tiles</param>
        /// <param name="numOfLayer">Die Anzahl der Ebenen</param>
        /// <param name="tileset">Das zu verwendende Tileset</param>
        public Map(int width, int height, int numOfLayer, Tileset tileset )
        {
            this.width = width;
            this.height = height;
            length = width * height;
            this.tileset = tileset;

            layer = new Layer[numOfLayer];

            for (int i = 0; i < numOfLayer; i++)
            {
                layer[i] = new Layer(width, height);
            }
        }

        /// <summary>
        /// Erstellt eine neue Karte anhand eines MapData Objekts.
        /// </summary>
        /// <param name="mapData">Das zu verwendende MapData Objekt</param>
        /// <param name="tileset">Das zu verwendende Tileset</param>
        public Map(MapData mapData, Tileset tileset)
        {
            width = mapData.Width;
            height = mapData.Height;
            length = width * height;
            this.tileset = tileset;

            layer = new Layer[mapData.Data.Length];

            for (int i = 0; i < mapData.Data.Length; i++)
            {
                layer[i] = new Layer(mapData.Width, mapData.Height, mapData.Data[i]);
            }

            nameToLayer = mapData.NameToLayer;
        }

        /// <summary>
        /// Prüft ob ein Objekt mit einem Layer kolledieren wird und gibt die Position an.
        /// </summary>
        /// <param name="position">Momentane Position des zu testenden Objekts</param>
        /// <param name="velocity">Geschwindikeitsvektor der zu testenden Objkts</param>
        /// <param name="width">Breite des zu testenden Objekts</param>
        /// <param name="height">Höhe des zu testenden Objekts</param>
        /// <param name="layer">Die Ebene gegen die getestet werden soll</param>
        /// <returns>Das Kollisionsergebniss</returns>
        public static TraceResult trace(Vector2 position, Vector2 velocity, int width, int height, Layer layer)
        {
            TraceResult traceResult;
            traceResult.resPosition = position;
            traceResult.tileX = 0;
            traceResult.tileY = 0;
            traceResult.collisionX = false;
            traceResult.collisionY = false;

            float steps = (float)Math.Ceiling(Math.Max(Math.Abs(velocity.Y), Math.Abs(velocity.X)) / (float)Game1.TILE_SIZE);

            if (steps > 1)
            {
                Vector2 step = velocity / steps;

                for (int i = 0; i < steps && (step.X != 0 || step.Y != 0); i++)
                {
                    traceResult = traceStep(traceResult, position, step, width, height, layer);
                    position = traceResult.resPosition;

                    if (traceResult.collisionX)
                    {
                        step.X = 0;
                    }

                    if (traceResult.collisionY)
                    {
                        step.Y = 0;
                    }
                }
            }
            else
            {
                return traceStep(traceResult, position, velocity, width, height, layer);
            }

            return traceResult;
        }

        /// <summary>
        /// Prüft ob ein Objekt mit einem Layer kolledieren wird und gibt die Position an.
        /// ( nur Schritte kleiner TILE_SIZE Pixel )
        /// </summary>
        /// <param name="position">Momentane Position des zu testenden Objekts</param>
        /// <param name="velocity">Geschwindikeitsvektor der zu testenden Objkts</param>
        /// <param name="width">Breite des zu testenden Objekts</param>
        /// <param name="height">Höhe des zu testenden Objekts</param>
        /// <param name="layer">Die Ebene gegen die getestet werden soll</param>
        /// <returns>Das Kollisionsergebniss</returns>
        private static TraceResult traceStep(TraceResult traceResult, Vector2 position, Vector2 velocity, int width, int height, Layer layer)
        {
            traceResult.resPosition += velocity;

            float velX = velocity.X;
            float velY = velocity.Y;

            float posX = position.X;
            float posY = position.Y;

            if (velX != 0)
            {
                float pxOffsetX = (velX > 0 ? width : 0);
                float tileOffsetX = (velX < 0 ? Game1.TILE_SIZE : 0);
                float firstTileY = (float)Math.Floor(posY / Game1.TILE_SIZE);
                float lastTileY = (float)Math.Ceiling((posY + height) / (float)Game1.TILE_SIZE);
                int tileX = (int)Math.Floor((posX + velX + pxOffsetX) / (float)Game1.TILE_SIZE);
                if (lastTileY >= 0 && firstTileY < layer.Height && tileX >= 0 && tileX < layer.Width)
                {
                    for (int tileY = (int)firstTileY; tileY < lastTileY; tileY++)
                    {
                        int id = tileY * layer.Width + tileX;
                        int collision = 0;
                        if (tileY * layer.Width + tileX >= 0 && tileY * layer.Width + tileX < layer.Data.Length)
                        {
                            collision = layer.Data[tileY * layer.Width + tileX];
                        }

                        if (collision != 0)
                        {
                            traceResult.collisionX = true;
                            traceResult.tileX = layer.Data[tileY * layer.Width + tileX];
                            traceResult.resPosition.X = tileX * Game1.TILE_SIZE - pxOffsetX + tileOffsetX;
                            break;
                        }
                    }
                }
            }

            if (velY != 0)
            {
                float pxOffsetY = (velY > 0 ? height : 0);
                float tileOffsetY = (velY < 0 ? Game1.TILE_SIZE : 0);
                float firstTileX = (float)Math.Floor(posX / Game1.TILE_SIZE);
                float lastTileX = (float)Math.Ceiling((double)(posX + width) / (float)Game1.TILE_SIZE);
                int tileY = (int)Math.Floor((posY + velY + pxOffsetY) / (float)Game1.TILE_SIZE);
                if (lastTileX >= 0 && firstTileX < layer.Width && tileY >= 0 && tileY < layer.Height)
                {
                    for (int tileX = (int)firstTileX; tileX < lastTileX; tileX++)
                    {
                        int id = tileY * layer.Width + tileX;
                        int collision = 0;
                        if (tileY * layer.Width + tileX >= 0 && tileY * layer.Width + tileX < layer.Data.Length)
                        {
                            collision = layer.Data[tileY * layer.Width + tileX];
                        }

                        if (collision != 0)
                        {
                            traceResult.collisionY = true;
                            traceResult.tileY = layer.Data[tileY * layer.Width + tileX];
                            traceResult.resPosition.Y = tileY * Game1.TILE_SIZE - pxOffsetY + tileOffsetY;
                            break;
                        }
                    }
                }
            }

            return traceResult;
        }

        /// <summary>
        /// Führt einen TileOperator für die Stellen auf der Karte aus, die von einem Rechteck geschnitten werden.
        /// </summary>
        /// <param name="rectangle">Das Rechteck</param>
        /// <param name="tileOperator">Der TileOperator</param>
        /// <returns></returns>
        public bool collideAndOperate(Rectangle rectangle, TileOperator tileOperator)
        {
            int posX1 = (int)rectangle.Left / Game1.TILE_SIZE;
            int posY1 = (int)rectangle.Top / Game1.TILE_SIZE;

            int posX2 = (int)rectangle.Right / Game1.TILE_SIZE;
            int posY2 = (int)rectangle.Bottom / Game1.TILE_SIZE;

            for (int iy = posY1; iy <= posY2; iy++)
            {
                for (int ix = posX1; ix <= posX2; ix++)
                {
                    tileOperator.Operate(ix, iy);
                }
            }
            return false;
        }
    }
}
